package com.code.train.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.fastjson2.JSON;
import com.code.train.context.LoginMemberContext;
import com.code.train.domain.*;
import com.code.train.enums.ConfirmOrderStatusEnum;
import com.code.train.enums.RedisKeyPreEnum;
import com.code.train.enums.SeatColEnum;
import com.code.train.enums.SeatTypeEnum;
import com.code.train.exception.BusinessException;
import com.code.train.exception.BusinessExceptionEnum;
import com.code.train.mapper.ConfirmOrderMapper;
import com.code.train.req.ConfirmOrderDoReq;
import com.code.train.req.ConfirmOrderQueryReq;
import com.code.train.req.ConfirmOrderTicketReq;
import com.code.train.resp.ConfirmOrderQueryResp;
import com.code.train.resp.PageResp;
import com.code.train.service.*;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

@Service
@Slf4j
public class ConfirmOrderServiceImpl implements ConfirmOrderService {

    @Autowired
    private ConfirmOrderMapper confirmOrderMapper;

    @Autowired
    private DailyTrainTicketService dailyTrainTicketService;

    @Autowired
    private DailyTrainCarriageService dailyTrainCarriageService;

    @Autowired
    private DailyTrainSeatService dailyTrainSeatService;

    @Autowired
    private AfterConfirmOrderService afterConfirmOrderService;

    @Autowired
    private RedissonClient redissonClient;

    @Autowired
    private SkTokenService skTokenService;

    @Override
    public void save(ConfirmOrderDoReq req) {
        DateTime now = DateTime.now();
        ConfirmOrder confirmOrder = BeanUtil.copyProperties(req, ConfirmOrder.class);
        if (ObjectUtil.isNull(confirmOrder.getId())) {
            confirmOrder.setId(System.currentTimeMillis());
            confirmOrder.setCreateTime(now);
            confirmOrder.setUpdateTime(now);
            confirmOrderMapper.insert(confirmOrder);
        } else {
            confirmOrder.setUpdateTime(now);
            confirmOrderMapper.updateByPrimaryKey(confirmOrder);
        }
    }

    @Override
    public PageResp<ConfirmOrderQueryResp> queryList(ConfirmOrderQueryReq req) {
        ConfirmOrderExample confirmOrderExample = new ConfirmOrderExample();
        confirmOrderExample.setOrderByClause("id desc");
        ConfirmOrderExample.Criteria criteria = confirmOrderExample.createCriteria();

        log.info("查询页码：{}", req.getPage());
        log.info("每页条数：{}", req.getSize());
        PageHelper.startPage(req.getPage(), req.getSize());
        List<ConfirmOrder> confirmOrderList = confirmOrderMapper.selectByExample(confirmOrderExample);

        PageInfo<ConfirmOrder> pageInfo = new PageInfo<>(confirmOrderList);
        log.info("总行数：{}", pageInfo.getTotal());
        log.info("总页数：{}", pageInfo.getPages());

        List<ConfirmOrderQueryResp> list = confirmOrderList.stream()
                .map(confirmOrder -> {
                    ConfirmOrderQueryResp resp = new ConfirmOrderQueryResp();
                    BeanUtils.copyProperties(confirmOrder, resp);
                    return resp;
                })
                .collect(Collectors.toList());

        PageResp<ConfirmOrderQueryResp> pageResp = new PageResp<>();
        pageResp.setTotal(pageInfo.getTotal());
        pageResp.setList(list);
        return pageResp;
    }

    @Override
    public void delete(Long id) {
        confirmOrderMapper.deleteByPrimaryKey(id);
    }

    @Override
    @SentinelResource(value = "doConfirm",blockHandler = "doConfirmBlock")
    public void doConfirm(ConfirmOrderDoReq req) {

        Boolean valid = skTokenService.valid(req.getDate(), req.getTrainCode(),LoginMemberContext.getId());
        if (!valid) {
            log.info("车次:{},令牌余票不足", req.getTrainCode());
            throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_TICKET_TOKEN_COUNT_ERROR);
        }

        log.info("车次为:{},购票流程开始",req.getTrainCode());

        String key = RedisKeyPreEnum.CONFIRM_ORDER+"-"+DateUtil.formatDate(req.getDate())+"-"+req.getTrainCode();

        RLock lock = null;
        try {
            lock = redissonClient.getLock(key);

            // 省略业务数据校验，如：车次是否存在，余票是否存在，车次是否在有效期内，tickets条数>0，同乘客同车次是否已买过
            //
            boolean tryLock = lock.tryLock(0, TimeUnit.SECONDS);
            if(tryLock){
                log.info("获取锁成功:{}",key);
            }else{
                throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_LOCK_ERROR);
            }
            String end = req.getEnd();
            String start = req.getEnd();
            String trainCode = req.getTrainCode();
            Date date = req.getDate();
            List<ConfirmOrderTicketReq> tickets = req.getTickets();
            // 保存确认订单表，状态初始
            DateTime now = DateTime.now();
            ConfirmOrder confirmOrder = new ConfirmOrder();
            confirmOrder.setId(IdUtil.getSnowflake(1, 1).nextId());
            confirmOrder.setCreateTime(now);
            confirmOrder.setUpdateTime(now);
            confirmOrder.setMemberId(LoginMemberContext.getId());
            confirmOrder.setDate(date);
            confirmOrder.setTrainCode(trainCode);
            confirmOrder.setStart(start);
            confirmOrder.setEnd(end);
            confirmOrder.setDailyTrainTicketId(req.getDailyTrainTicketId());
            confirmOrder.setStatus(ConfirmOrderStatusEnum.INIT.getCode());
            confirmOrder.setTickets(JSON.toJSONString(tickets));
            confirmOrderMapper.insert(confirmOrder);

            // 查出余票记录，需要得到真实的库存
            DailyTrainTicket dailyTrainTicket = dailyTrainTicketService.selectByUnique(date, trainCode, start, end);
            log.info("查出余票记录: {}", dailyTrainTicket);

            //预扣减余票数量，并判断余票是否足够
            reduceTickets(req, dailyTrainTicket);
            // 最终的选座结果
            List<DailyTrainSeat> finalSeatList = new ArrayList<>();
            // 计算相对第一个座位的偏移值
            // 比如选择的是C1,D2，则偏移值是：[0,5]
            // 比如选择的是A1,B1,C1，则偏移值是：[0,1,2]
            ConfirmOrderTicketReq ticketReq0 = tickets.get(0);
            if (StrUtil.isNotBlank(ticketReq0.getSeat())) {
                log.info("本次购票有选座");
                // 查出本次选座的座位类型都有哪些列，用于计算所选座位与第一个座位的偏离值
                List<SeatColEnum> colEnumList = SeatColEnum.getColsByType(ticketReq0.getSeatTypeCode());
                log.info("本次选座的座位类型包含的列：{}", colEnumList);
                // 组成和前端两排选座一样的列表，用于作参照的座位列表，例：referSeatList = {A1, C1, D1, F1, A2, C2, D2, F2}
                List<String> referSeatList = new ArrayList<>();
                for (int i = 1; i <= 2; i++) {
                    for (SeatColEnum seatColEnum : colEnumList) {
                        referSeatList.add(seatColEnum.getCode() + i);
                    }
                }
                log.info("用于作参照的两排座位：{}", referSeatList);
                List<Integer> offsetList = new ArrayList<>();
                // 绝对偏移值，即：在参照座位列表中的位置
                List<Integer> aboluteOffsetList = new ArrayList<>();
                for (ConfirmOrderTicketReq ticket : tickets) {
                    int index = referSeatList.indexOf(ticket.getSeat());
                    aboluteOffsetList.add(index);
                }
                log.info("计算得到所有座位的绝对偏移值：{}", aboluteOffsetList);
                for (Integer index : aboluteOffsetList) {
                    int offset = index - aboluteOffsetList.get(0);
                    offsetList.add(offset);
                }
                log.info("计算得到所有座位的相对第一个座位的偏移值：{}", offsetList);

                getSeat(finalSeatList,
                        date,
                        trainCode,
                        ticketReq0.getSeatTypeCode(),
                        ticketReq0.getSeat().split("")[0], // 从A1得到A
                        offsetList,
                        dailyTrainTicket.getStartIndex(),
                        dailyTrainTicket.getEndIndex()
                );
            } else {
                log.info("本次购票没有选座");
                for (ConfirmOrderTicketReq ticketReq : tickets) {
                    getSeat(finalSeatList,
                            date,
                            trainCode,
                            ticketReq.getSeatTypeCode(),
                            null,
                            null,
                            dailyTrainTicket.getStartIndex(),
                            dailyTrainTicket.getEndIndex()
                    );
                }
            }
            log.info("最终选座：{}", finalSeatList);
            // 选中座位后事务处理：
            // 座位表修改售卖情况sell；
            // 余票详情表修改余票；
            // 为会员增加购票记录
            // 更新确认订单为成功
            try {
                afterConfirmOrderService.afterDoConfirm(dailyTrainTicket, finalSeatList, tickets, confirmOrder);
            } catch (Exception e) {
                log.error("保存购票信息异常", e);
                throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_AFTER_ERROR);
            }

        } catch (InterruptedException e) {
            throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_LOCK_ERROR);
        } finally {
            log.info("购票流程结束,释放锁:{}",key);
            if(lock!=null&&lock.isHeldByCurrentThread()){
                lock.unlock();
            }
        }

    }
    private void getSeat(List<DailyTrainSeat> finalSeatList,Date date, String trainCode, String seatType, String column, List<Integer> offsetList,Integer start,Integer end) {
        List<DailyTrainSeat> getSeatList = new ArrayList<>();
        List<DailyTrainCarriage> carriageList = dailyTrainCarriageService.selectBySeatType(date, trainCode, seatType);
        log.info("共查出{}个符合条件的车厢", carriageList.size());

        // 一个车箱一个车箱的获取座位数据
        for (DailyTrainCarriage dailyTrainCarriage : carriageList) {
            log.info("开始从车厢{}选座", dailyTrainCarriage.getIndex());
            getSeatList = new ArrayList<>();
            List<DailyTrainSeat> seatList = dailyTrainSeatService.selectByCarriage(date, trainCode, dailyTrainCarriage.getIndex());
            log.info("车厢{}的座位数：{}", dailyTrainCarriage.getIndex(), seatList.size());

            for (DailyTrainSeat dailyTrainSeat : seatList) {
                Integer seatIndex = dailyTrainSeat.getCarriageSeatIndex();
                String col = dailyTrainSeat.getCol();

                // 判断当前座位不能被选中过
                boolean alreadyChooseFlag = false;
                for (DailyTrainSeat finalSeat : finalSeatList){
                    if (finalSeat.getId().equals(dailyTrainSeat.getId())) {
                        alreadyChooseFlag = true;
                        break;
                    }
                }
                if (alreadyChooseFlag) {
                    log.info("座位{}被选中过，不能重复选中，继续判断下一个座位", seatIndex);
                    continue;
                }

                //判断column 有值的话要比对列号
                if (StrUtil.isNotBlank(column)) {
                    if (!column.equals(col)) {
                        log.info("座位{}列值不对，继续判断下一个座位，当前列值：{}，目标列值：{}", seatIndex, col, column);
                        continue;
                    }
                } else {
                    log.info("无选座");
                }

                boolean isChoose = calSell(dailyTrainSeat, start, end);
                if (isChoose) {
                    log.info("座位{}被选中", seatIndex);
                    getSeatList.add(dailyTrainSeat);
                } else {
                    continue;
                }

                //根据offset选择剩下的座位
                boolean isGetAllOffsetSeat = true;
                if (ObjectUtil.isNotEmpty(offsetList)) {
                    log.info("有偏移值：{}，校验偏移的座位是否可选", offsetList);
                    //判断索引对应的每个座位
                    for (int j = 1; j < offsetList.size(); j++) {
                        Integer offset = seatIndex + offsetList.get(j);
                        //座位在库中的索引从一开始
                        //获取绝对位置
                        int nextIndex = seatIndex + offset - 1;
                        // 有选座时，一定是在同一个车箱
                        if (nextIndex >= seatList.size()) {
                            log.info("座位{}不可选，偏移后的索引超出了这个车箱的座位数", nextIndex);
                            isGetAllOffsetSeat = false;
                            break;
                        }
                        //获取偏移量对应的座位
                        DailyTrainSeat nextDailyTrainSeat = seatList.get(nextIndex);
                        boolean isChooseNext = calSell(nextDailyTrainSeat, start, end);
                        if (isChooseNext) {
                            log.info("座位{}被选中", nextIndex);
                            getSeatList.add(dailyTrainSeat);
                        } else {
                            log.info("座位{}不可选", nextDailyTrainSeat.getCarriageSeatIndex());
                            isGetAllOffsetSeat = false;
                            break;
                        }
                    }
                }
                if (!isGetAllOffsetSeat) {
                    getSeatList = new ArrayList<>();
                    continue;
                }

                // 保存选好的座位
                finalSeatList.addAll(getSeatList);
                return;
            }
        }
    }
    /**
     * 计算某座位在区间内是否可卖
     * 例：sell=10001，本次购买区间站1~4，则区间已售000
     * 全部是0，表示这个区间可买；只要有1，就表示区间内已售过票
     *
     * 选中后，要计算购票后的sell，比如原来是10001，本次购买区间站1~4
     * 方案：构造本次购票造成的售卖信息01110，和原sell 10001按位与，最终得到11111
     */
    private boolean calSell(DailyTrainSeat dailyTrainSeat,Integer start,Integer end) {
        String sell = dailyTrainSeat.getSell();
        String sellPart = sell.substring(start, end);
        if(Integer.parseInt(sellPart)>0){
            log.info("车次为{},车厢为{},座位{}不可卖", dailyTrainSeat.getTrainCode(), dailyTrainSeat.getCarriageIndex(),dailyTrainSeat.getRow() + dailyTrainSeat.getCol());
            return false;
        }else{
            log.info("车次为{},车厢为{},座位{}可卖", dailyTrainSeat.getTrainCode(), dailyTrainSeat.getCarriageIndex(),dailyTrainSeat.getRow() + dailyTrainSeat.getCol());
            String curSell = sellPart.replace("0", "1");
            curSell = StrUtil.fillBefore(curSell, '0', end);
            curSell = StrUtil.fillAfter(curSell, '0', sell.length());

            int newShellInt = NumberUtil.binaryToInt(curSell) | NumberUtil.binaryToInt(sell);
            String newSell = NumberUtil.getBinaryStr(newShellInt);
            newSell = StrUtil.fillBefore(newSell, '0', sell.length());
            log.info("座位{}被选中，原售票信息：{}，车站区间：{}~{}，即：{}，最终售票信息：{}"
                    , dailyTrainSeat.getCarriageSeatIndex(), sell, start, end, curSell, newSell);
            dailyTrainSeat.setSell(newSell);
            return true;
        }
    }

    private static void reduceTickets(ConfirmOrderDoReq req, DailyTrainTicket dailyTrainTicket) {
        for (ConfirmOrderTicketReq ticketReq : req.getTickets()) {
            String seatTypeCode = ticketReq.getSeatTypeCode();
            SeatTypeEnum seatType = SeatTypeEnum.getEnumByCode(seatTypeCode);
            if (seatType == null) {
                throw new RuntimeException("无效的座位类型代码: " + seatTypeCode);
            }
            log.info("座位类型: {}", seatType.getDesc());
            switch (seatType) {
                case YDZ -> {
                    int countLeft = dailyTrainTicket.getYdz() - 1;
                    if (countLeft < 0) {
                        throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_TICKET_COUNT_ERROR);
                    }
                    dailyTrainTicket.setYdz(countLeft);
                }
                case EDZ -> {
                    int countLeft = dailyTrainTicket.getEdz() - 1;
                    if (countLeft < 0) {
                        throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_TICKET_COUNT_ERROR);
                    }
                    dailyTrainTicket.setEdz(countLeft);
                }
                case RW -> {
                    int countLeft = dailyTrainTicket.getRw() - 1;
                    if (countLeft < 0) {
                        throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_TICKET_COUNT_ERROR);
                    }
                    dailyTrainTicket.setRw(countLeft);
                }
                case YW -> {
                    int countLeft = dailyTrainTicket.getYw() - 1;
                    if (countLeft < 0) {
                        throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_TICKET_COUNT_ERROR);
                    }
                    dailyTrainTicket.setYw(countLeft);
                }
            }
        }
    }

    /**
     * 降级方法，需包含限流方法的所有参数和BlockException参数
     * @param req
     * @param e
     */
    public void doConfirmBlock(ConfirmOrderDoReq req, BlockException e) {
        log.info("购票请求被限流：{}", req);
        throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_FLOW_EXCEPTION);
    }
}